package de.dnb.gnd.parser;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;

import javax.naming.OperationNotSupportedException;

import de.dnb.basics.Constants;
import de.dnb.basics.applicationComponents.StreamUtils;
import de.dnb.basics.applicationComponents.StringUtils;
import de.dnb.basics.applicationComponents.Quadruplett;
import de.dnb.basics.applicationComponents.RangeCheckUtils;
import de.dnb.gnd.exceptions.ExceptionHandler;
import de.dnb.gnd.exceptions.IgnoringHandler;
import de.dnb.gnd.exceptions.IllFormattedLineException;
import de.dnb.gnd.parser.line.Line;
import de.dnb.gnd.parser.line.LineParser;
import de.dnb.gnd.parser.tag.BibTagDB;
import de.dnb.gnd.parser.tag.GNDTagDB;
import de.dnb.gnd.parser.tag.Tag;
import de.dnb.gnd.parser.tag.TagDB;
import de.dnb.gnd.utils.RecordUtils;

/**
 * 
 * @author baumann
 *	
 *	Nimmt an, dass ein {@link BufferedReader} Datensätze  enthält. 
 *
 *	Der Datensatz besteht aus Feldern, diese wiederum aus Zeilen bestehen.
 *	
 *
 */
public final class RecordParser {

    // --- Default-Werte: ----	

    public RecordParser(
        TagDB tagDB,
        ExceptionHandler handler,
        String lineDelimiter) {
        this.handler = handler;
        this.defaultTagDB = tagDB;
        this.lineDelimiter = lineDelimiter;
    }

    /**
     * Mit GND-DB, Ignorierendem Handler und (CR)?LF.
     */
    public RecordParser() {
        this(GNDTagDB.getDB(), new IgnoringHandler(), "(\r)?\n");
    }

    public static final TagDB BIB_TAG_DB = BibTagDB.getDB();

    public static final TagDB AUTH_TAG_DB = GNDTagDB.getDB();

    private ExceptionHandler handler;

    private Record record;

    /**
     * Zum Parsen verwendet
     */
    private TagDB theTagDB;

    private TagDB defaultTagDB;

    /**
     * UNIX: LF
     * Windows: CR LF.
     */
    private String lineDelimiter = "(\r)?\n";

    /**
     * wenn true, werden MARC-Felder überlesen. false ist die 
     * Standardeinstellung für IBW-Downloads.
     */
    private boolean ignoreMARC = false;

    /**
     * Ändert die Default-Tag-Datenbank (Wird verwendet, wenn der Satztyp 
     * nicht sicher erkannt werden kann).
     * 
     * @param aTagDB the tag database to set, not null.
     */
    public void setDefaultTagDB(final TagDB aTagDB) {
        RangeCheckUtils.assertReferenceParamNotNull("aTagDB", aTagDB);
        this.defaultTagDB = aTagDB;
    }

    /**
     * Regulärer Ausdruck als String. Ändert den Zeilentrenner. 
     * 
     * @param aLineDelimiter the lineDelimiter to set. Not null, not empty.
     */
    public void setLineDelimiter(final String aLineDelimiter) {
        RangeCheckUtils.assertStringParamNotNullOrEmpty("alineDelimiter",
            aLineDelimiter);
        this.lineDelimiter = aLineDelimiter;
    }

    public boolean isIgnoreMARC() {
        return ignoreMARC;
    }

    /**
     * Überlese beim Parsen von Pica+-Daten die Felder, die in
     * MARC21 üblich sind, die aber nicht zum Pica-Format gehören.
     * 
     * @param ignoreMARC	beliebig, wenn true, werden MARC-Felder
     * 						überlesen. false ist die Standardeinstellung
     * 						für IBW-Downloads.
     */
    public void setIgnoreMARC(boolean ignoreMARC) {
        this.ignoreMARC = ignoreMARC;
    }

    /**
     * Grundlegende Methode. 
     * 
     * Geht nicht davon aus, dass eine IDN in lines enthalten ist. 
     * Die idn ist schon anderweitig ermittelt worden
     * 
     *
     * @param idn	idn oder null.
     * @param lines nicht null
     * @return		Gültigen Datensatz
     *
     * 				
     */
    public Record parse(final String idn, final Iterable<String> lines) {

        RangeCheckUtils.assertReferenceParamNotNull("lines", lines);
        // Initialisieren der Variablen		
        record = new Record(idn, theTagDB);

        // reader Zeile für Zeile lesen und analysieren:
        for (String lineStr : lines) {
            if (lineStr != null) {
                if (lineStr.length() != 0) {
                    processLine(lineStr, idn);
                }
                record.rawData += lineStr + Constants.LINE_SEPARATOR;
            }
        }
        return record;
    }

    /**
     * Parst einen String auf darin enthaltenen Datensatz. Die
     * idn wird nach dem üblichen Muster extrahiert. Danach erfolgt
     * eine Zerlegung in Zeilen nach dem festgelegten lineDelimiter.
     * Dieser ist per Default \r\n, kann aber durch  
     * {@link #setLineDelimiter(String)} geändert werden.
     * 
     * @param aRecord	nicht null
     * @return			nicht null
     */
    public Record parse(final CharSequence aRecord) {
        RangeCheckUtils.assertReferenceParamNotNull("aRecord", aRecord);
        String idn = StringUtils.extractIDN(aRecord);
        if (RecordUtils.containsRecordType(aRecord)) {
            if (RecordUtils.isAuthority(aRecord))
                theTagDB = AUTH_TAG_DB;
            else
                theTagDB = BIB_TAG_DB;
        } else
            theTagDB = defaultTagDB;

        Pattern pattern = Pattern.compile(lineDelimiter);
        String[] lineArr = pattern.split(aRecord);
        List<String> lines = Arrays.asList(lineArr);
        return parse(idn, lines);
    }

    // ----- Hilfsfunktionen: -----------------------------

    /**
     * Verarbeitet line und fügt diese im Erfolgsfalle record hinzu.
     * Eventuelle Fehler werden von handler verarbeitet.
     * 
     * @param lineStr	Eine nicht leere Zeile, eventuell mit einem Tag  
     * 					beginnend.
     * @param idn		idn zur Fehleranalyse
     * 
     */
    private void processLine(final String lineStr, final String idn) {

        RangeCheckUtils.assertReferenceParamNotNull("lineStr", lineStr);

        boolean controlFound = controlFieldsFoundAndProcessed(lineStr);
        if (controlFound) { // dann Zeile schon verarbeitet
            return;
        }
        // also noch zu verarbeiten, Versuch:
        Line line = null;
        try {
            line = LineParser.parse(lineStr, theTagDB, ignoreMARC);
        } catch (Exception e) {
            handler.handle(e, "Datensatz mit idn " + idn + ": Zeile '" + lineStr
                + "' nicht richtig gebildet");
        }
        if (line != null) { // Neuer tag
            try {
                record.add(line);
            } catch (OperationNotSupportedException e) {
                handler.handle(e, "Datensatz mit idn " + idn
                    + ": Addition der Zeile nicht erlaubt");
            }

            return;
        }
    }

    /**
     * Verarbeitet die 3 Kontrollfelder 
     * 001A	= 001 (GND) = 0200 (Titel)
     * 001B = 002		= 0210
     * 001D = 003		= 0230
     * und gibt zurück, ob das möglich war.
     * 
     * @param lineStr	nicht null, nicht leer.
     * @return			Zeile mit Kontrollfeldern gefunden(immer 2. Zeile bei
     * 					download-Daten).
     */
    private boolean controlFieldsFoundAndProcessed(final String lineStr) {

        RangeCheckUtils.assertReferenceParamNotNull("lineStr", lineStr);
        //@formatter:off
		Quadruplett<String, String, String, String> 
			controlFields = StringUtils.getControlFields(lineStr);
		//@formatter:on
        if (controlFields != null) {

            try {
                Tag tagC = theTagDB.findTag("001A");
                String lineString = controlFields.first;
                Line line =
                    LineParser
                        .parse(tagC, Format.PICA3, lineString, ignoreMARC);
                record.add(line);

                tagC = theTagDB.findTag("001B");
                // millis werden automatisch erzeugt:
                lineString =
                    controlFields.second + "$t" + controlFields.third + ".000";
                line =
                    LineParser
                        .parse(tagC, Format.PICA3, lineString, ignoreMARC);
                record.add(line);

                tagC = theTagDB.findTag("001D");
                lineString = controlFields.forth;
                line =
                    LineParser
                        .parse(tagC, Format.PICA3, lineString, ignoreMARC);
                record.add(line);
            } catch (Exception e) {
                // OperationNotSupported von record.add()
                // IllFormatedLine       von createLine()
                handler.handle(e, "Fehler in Control Fields");
            }

            return true;
        } else {

            return false;
        }
    }

    /**
     * Setzt den Exceptionhandler, der Aktionen durchführt, wenn
     * Fehler beim Parsen auftreten. Der Default ist IgnoringHandler. 
     * 
     * @param aHandler   Wenn handler == null, wird nichts geändert.
     * 
     */
    public void setHandler(final ExceptionHandler aHandler) {
        if (aHandler != null)
            this.handler = aHandler;
    }

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {

        RecordParser parser = new RecordParser();

        Record record = parser.parse("");
        System.out.println(record);
    }

}
